package com.example.api.util;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.api.exception.GoffException;
import com.example.api.model.TokenBucket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.HashOperations;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import com.alibaba.fastjson.JSON;

/**
 * @program: api_limit
 * @description: API接口限制
 * @author: Mr.chen
 * @create: 2020-02-18 18:58
 **/
@Component
@Slf4j
public class RateLimiterUtil {

    /**
     * redis key前缀
     */
    private static String KEY_PREFIX = "do1.api.limit";
    /**
     * redis key 分隔符
     */
    private static String SEPARATOR = ":";

    @Resource
    private RedisTemplate<String,String> redisTemplate;
    private static RedisTemplate<String,String> redisTemplateStatic;

    @PostConstruct
    public void init(){
        redisTemplateStatic = redisTemplate;
    }

    /**
     * 请求限制 token检查
     * @param apiName
     * @param userId
     * @return
     * @throws Exception
     */
    public static  boolean accessCheck(String apiName, String userId, Integer accessLimit, Long accessLimitInterval) throws Exception {
        Optional.ofNullable(accessLimit).orElseThrow(() ->new GoffException(-1,"接口限制次数不能为空"));
        Optional.ofNullable(accessLimitInterval).orElseThrow(() ->new GoffException(-1,"接口限制时间不能为空"));

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String apiKey = apiKeyGenerator(apiName);
        TokenBucket tb = getTokenBucket(apiName, userId);

        if (null == tb) {
            //用户从未请求过
            log.debug("[请求限制 - token检查]当前请求key:{},userId:{},最后填充时间:{},剩余token数量:{},是否放行请求:{}",
                    apiKey, userId, sdf.format(new Date()), accessLimit, true);
            return true;
        } else {
            if (isRefreshLimit(tb.getLastRefillTime(), accessLimitInterval)) {
                log.debug("[请求限制 - token检查]当前请求key:{},userId:{},最后填充时间:{},剩余token数量:{},是否放行请求:{}",
                        apiKey, userId, sdf.format(new Date(tb.getLastRefillTime())), accessLimit, true);
                return true;
            } else {
                boolean hasToken = tb.getTokensRemaining() > 0;
                log.debug("[请求限制 - token检查]当前请求key:{}, userId:{}, token最后填充时间:{}, 剩余token数量:{}, 是否放行请求:{}",
                        apiKey, userId, sdf.format(new Date(tb.getLastRefillTime())), tb.getTokensRemaining(), hasToken);
                return hasToken;
            }
        }
    }

    /**
     * 请求限制 token消费
     * <p>注意,根据需求评审后,允许请求成功数有出入,所以不需要保证线程安全</p>
     * @param apiName
     * @param userId
     * @throws Exception
     */
    public static void accessConsume(String apiName, String userId, Integer accessLimit, Long accessLimitInterval) throws Exception {
        Optional.ofNullable(accessLimit).orElseThrow(() ->new GoffException(-1,"接口限制次数不能为空"));
        Optional.ofNullable(accessLimitInterval).orElseThrow(() ->new GoffException(-1,"接口限制时间不能为空"));

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String apiKey = apiKeyGenerator(apiName);
        TokenBucket tb = getTokenBucket(apiName, userId);

        if (null == tb) {
            //用户从未请求过
            tb = new TokenBucket(System.currentTimeMillis(), accessLimit - 1);
        } else {
            //当前令牌剩余数量, 允许出入所以检查是否大于0
            if (isRefreshLimit(tb.getLastRefillTime(), accessLimitInterval)) {
                tb.setLastRefillTime(System.currentTimeMillis());
                tb.setTokensRemaining(accessLimit - 1);
            } else {
                int currentTokensRemaining = (tb.getTokensRemaining() - 1) >= 0 ? tb.getTokensRemaining() - 1 : tb.getTokensRemaining();
                tb.setTokensRemaining(currentTokensRemaining);
            }
        }

        putHashValue(apiKey, userId, tb);
        log.debug("[请求限制 - token消费]当前请求key:{}, userId:{}, token最后填充时间:{}, 剩余token数量:{}",
                apiKey, userId, sdf.format(new Date(tb.getLastRefillTime())), tb.getTokensRemaining());
    }

    /**
     * 现在时间的时间差 到 最后一次填充Token的时间
     *
     * <p>[现在时间的时间差 到 最后一次填充Token的时间] > [接口限制时间间隔] , 返回:true , 表示 API接口限制数已经刷新</p>
     * <p>[现在时间的时间差 到 最后一次填充Token的时间] ≤ [接口限制时间间隔] , 返回:false , 表示 API接口限制数将继续使用</p>
     * @param LastRefillTime 令牌的最后填充时间
     * @param accessLimitInterval 接口限制时间间隔(毫秒)
     * @return
     */
    private static boolean isRefreshLimit(long LastRefillTime, long accessLimitInterval) {
        //现在时间的时间差 到 最后一次填充Token的时间
        long intervalSinceLast = System.currentTimeMillis() - LastRefillTime;
        return intervalSinceLast > accessLimitInterval ? true : false;
    }

    /**
     * 根据命名规则获取令牌
     * @param apiName api接口名称
     * @param userId 用户id
     * @return
     * @throws Exception
     */
    private static TokenBucket getTokenBucket(String apiName, String userId) throws Exception{
        Optional.ofNullable(apiName).orElseThrow(() ->new GoffException(-1,"接口名称不能为空"));
        Optional.ofNullable(userId).orElseThrow(() ->new GoffException(-1,"用户id不能为空"));

        return getHashValue(apiKeyGenerator(apiName), userId, TokenBucket.class);

    }


    /**
     * API Key 生成器
     * @param apiName
     * @return
     */
    private static  String apiKeyGenerator(String apiName) {
        StringBuilder sb = new StringBuilder();
        sb.append(KEY_PREFIX).append(SEPARATOR).append(apiName);
        return sb.toString();
    }

    /**
     * 封装hash操作
     * @return
     */
    private static  HashOperations<String,String,String> getHashOperations(){
        return redisTemplateStatic.opsForHash();
    }

    /**
     * 封装hash操作
     * @param key
     * @param hashKey
     * @return
     */
    private static  String getHashValue(String key,String hashKey){
        HashOperations<String,String,String> hashOperations = getHashOperations();
        return hashOperations.get(key,hashKey);
    }

    /**
     * 封装hash操作
     * @param key
     * @param hashKey
     * @param clazz
     * @param <T>
     * @return
     */
    public static  <T> T getHashValue(String key, String hashKey, Class<T> clazz){
        String value = getHashValue(key,hashKey);
        if (null != value && value.trim().length() > 0) {
            return JSON.parseObject(value, clazz);
        } else {
            return null;
        }
    }

    /**
     * 封装hash操作
     * @param key
     * @param hashKey
     * @param value
     */
    public static  void putHashValue(String key,String hashKey,Object value){
        HashOperations<String,String,String> hashOperations = getHashOperations();
        if (value instanceof String){
            hashOperations.put(key,hashKey, (String) value);
        }else {
            hashOperations.put(key,hashKey, JSON.toJSONString(value, SerializerFeature.WriteNullStringAsEmpty));
        }
    }

}
